package joe;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class SceneCamera extends SceneNode {
	private @Nullable SceneNode renderNode;
	private @Nullable Render.Frame renderFrame, curRenderFrame;
	private float vx, vy, vw = 1, vh = 1; // [0,1]. (0,0):(left,bottom)
	private final Mat4 viewMat = new Mat4().identity();
	private final Mat4 projMat = new Mat4().identity();
	private final Mat4 viewProjMat = new Mat4().identity(); // default: pos:(0,0,0) dir:+z up:+y
	private float widthFov = 2, heightRatio = 2, near = 0, far = 1;
	private boolean frustum;
	private boolean cullBack = true, depthTest = true, blend = true;
	private boolean projDirty;

	public SceneCamera(@Nullable String name) {
		super(name);
	}

	public @Nullable SceneNode getRenderNode() {
		return renderNode;
	}

	public @NotNull SceneCamera setRenderNode(@Nullable SceneNode renderNode) {
		this.renderNode = renderNode;
		return this;
	}

	public @Nullable Render.Frame getRenderFrame() {
		return renderFrame;
	}

	public void setRenderFrame(@Nullable Render.Frame renderFrame) {
		this.renderFrame = renderFrame;
	}

	public @Nullable Render.Frame getCurRenderFrame() {
		return curRenderFrame;
	}

	public @NotNull SceneCamera setViewport(float x, float y, float width, float height) {
		vx = x;
		vy = y;
		vw = width;
		vh = height;
		return this;
	}

	public @NotNull Mat4 getProjMat() {
		return projMat;
	}

	public @NotNull Mat4 getViewMat() {
		return viewMat;
	}

	public @NotNull Mat4 getViewProjMat() {
		return viewProjMat;
	}

	public boolean isFrustum() {
		return frustum;
	}

	public float getWidthFov() {
		return widthFov;
	}

	public float getHeightRatio() {
		return heightRatio;
	}

	public float getNear() {
		return near;
	}

	public float getFar() {
		return far;
	}

	public @NotNull SceneCamera setOrtho(float width, float height, float near, float far) {
		widthFov = width;
		heightRatio = height;
		this.near = near;
		this.far = far;
		frustum = false;
		projDirty = true;
		return this;
	}

	public @NotNull SceneCamera setFrustum(float fov, float ratio, float near, float far) {
		widthFov = fov;
		heightRatio = ratio;
		this.near = near;
		this.far = far;
		frustum = true;
		projDirty = true;
		return this;
	}

	public boolean isCullBack() {
		return cullBack;
	}

	public @NotNull SceneCamera setCullBack(boolean cullBack) {
		this.cullBack = cullBack;
		return this;
	}

	public boolean isDepthTest() {
		return depthTest;
	}

	public @NotNull SceneCamera setDepthTest(boolean depthTest) {
		this.depthTest = depthTest;
		return this;
	}

	public boolean isBlend() {
		return blend;
	}

	public @NotNull SceneCamera setBlend(boolean blend) {
		this.blend = blend;
		return this;
	}

	private void updateProjView() {
		var pm = projMat;
		if (projDirty) {
			projDirty = false;
			if (frustum)
				pm.frustum(widthFov, heightRatio, near, far);
			else
				pm.ortho(widthFov, heightRatio, near, far);
		}
		// pos: (0,0,0,1) * mm = (t12,t13, t14)
		// dir: (0,0,1,0) * mm = (t8 ,t9 , t10)
		// up : (0,1,0,0) * mm = (t4 ,t5 , t6 )
		var tm = getModelMat();
		viewProjMat.mul(viewMat.lookAt(tm.get(12), tm.get(13), tm.get(14),
				tm.get(8), tm.get(9), tm.get(10), tm.get(4), tm.get(5), tm.get(6)), pm);
	}

	@Override
	protected void updateModelMat(@Nullable Mat4 parentTranMat) {
		super.updateModelMat(parentTranMat);
		updateProjView();
	}

	protected void applyCamera(@NotNull Render render) {
		int w = render.getWindowWidth();
		int h = render.getWindowHeight();
		render.setViewport((int)(w * vx), (int)(h * vy), (int)(w * vw), (int)(h * vh));
		render.setCullBack(cullBack);
		render.setDepthTest(depthTest);
		render.setBlend(blend);
	}

	@Override
	protected void onRender(@NotNull Render render) {
		if (projDirty)
			updateProjView();
		var engine = Engine.current();
		var oldCam = engine.setCurCamera(this);
		var oldFrame = oldCam != null ? oldCam.curRenderFrame : null;
		var frame = renderFrame;
		try {
			if (frame != null) {
				curRenderFrame = frame;
				frame.use();
			}
			applyCamera(render);
			var rn = renderNode;
			if (rn != null)
				rn.onRender(render);
			else
				super.onRender(render);
		} finally {
			if (frame != null) {
				if (oldFrame != null)
					oldFrame.use();
				else
					frame.unuse();
				curRenderFrame = null;
			}
			if (oldCam != null)
				oldCam.applyCamera(render);
			engine.setCurCamera(oldCam);
		}
	}
}
